#include "dominion.h"
#include "dominion_helpers.h"
#include "interface.h"
#include "rngs.h"
#include <stdio.h>
#include <math.h>
#include <stdlib.h>
#include <stdbool.h>
#include <sys/time.h>
#include <time.h>
#include <assert.h>
#include <getopt.h>
#include <string.h>

#define MAX_CARD (treasure_map + 1)
#define MIN_KINGDOM adventurer
#define NUM_KINGDOM (MAX_CARD - MIN_KINGDOM)
#define NUM_SUPPLY 10
#define MAX_COST 9

/*
USAGE

To do a test run with a random seed and only output log entries for turns that
fail to stdout:
    ./testdominion

To output log entries for all turns to stdout:
    ./testdominion -v

To use a specific seed:
    ./testdomion -s [integer]

NOTE: regardless of the presence of -v, a FULL (verbose) log is ALWAYS
outputted to gameResults.out.

All options can be used together.
*/

static bool verbose = false;

static FILE *gameresults_out_file = NULL;

#define verbose_printf(...) \
{\
    if (verbose)\
        printf (__VA_ARGS__);\
    fflush (stdout);\
    fprintf (gameresults_out_file, __VA_ARGS__);\
    fflush (gameresults_out_file);\
}

#define always_printf(...) \
{\
    printf (__VA_ARGS__);\
    fflush (stdout);\
    fprintf (gameresults_out_file, __VA_ARGS__);\
    fflush (gameresults_out_file);\
}

// For appending lines together into a single log entry string
static char turn_log[16384];

#define log_flush() \
{\
    fprintf (gameresults_out_file, "%s", turn_log);\
    fflush (gameresults_out_file);\
    memset (turn_log, 0, sizeof(turn_log));\
}

#define log_append(...) \
    sprintf (strchr (turn_log, '\0'), __VA_ARGS__)

#define log_print() \
    printf ("%s", turn_log)

// Make this global for use when testing the Embargo card.
int kingdom[NUM_SUPPLY];

// Returns -1 if no card in the specified range is available at the specified
// cost or cheaper.
int pick_buyable_card (int card_range_min, int card_range_max, int max_cost, struct gameState *g)
{
    // Generate a list of possible cards to buy.
    int supply_cards[MAX_CARD+1];
    int num_buyable_cards = 0;
    
    if (card_range_min == -1)
    {
        // Add a random chance of choosing not to buy a card at all.
        supply_cards[num_buyable_cards++] = -1;
        card_range_min = 0;
    }
    
    for (int card = card_range_min; card < card_range_max; card++)
    {
        if (getCost (card) <= max_cost && g->supplyCount[card] > 0)
            // card is available and we can afford it
            supply_cards[num_buyable_cards++] = card;
    }
    
    if (num_buyable_cards == 0)
        // No card satesfies the criteria given
        return -1;
    
    // Pick what card to buy.
    return supply_cards[rand () % num_buyable_cards];
}

// Returns MAX_COST if there are no available cards in the specified range.
int cheapest_buyable_card_cost (int card_range_min, int card_range_max, struct gameState *g)
{
    int ret = MAX_COST;
    
    for (int card = card_range_min; card < card_range_max; card++)
    {
        int cost = getCost (card);
        if (g->supplyCount[card] > 0 && cost < ret)
            ret = cost;
    }
    
    return ret;
}

void run_turn_action_phase (struct gameState *g)
{
    int action_card_positions[MAX_HAND];
    int remodelable_card_positions[MAX_HAND];
    int minable_card_positions[MAX_HAND];
    int cplayer = g->whoseTurn;
    
    // value expected in g->playedCardCount at end of turn
    int expected_numplayed = 0; 
    // number of cards besides played action cards that the player is expected
    // to have at end of turn
    int expected_deckcount =    g->handCount[cplayer] +
                                g->deckCount[cplayer] +
                                g->discardCount[cplayer];
    
    while (true) // Loop because there may be multiple action cards to play
    {
        if (g->numActions == 0)
            return;
        
        // For remodel card
        int cheapest_available_card_cost = cheapest_buyable_card_cost (0, MAX_CARD, g);
        
        // For mine card
        int cheapest_available_treasure_cost = cheapest_buyable_card_cost (copper, gold + 1, g);
        
        // Generate a list of possible action cards to play.
        int num_action_cards = 0, num_remodelable_cards = 0,
            num_minable_cards = 0;
        int treasuremap_idx1 = -1, treasuremap_idx2 = -1, remodel_idx = -1,
            mine_idx = -1, salvager_idx = -1, ambassador_idx = -1,
            feast_idx = -1;
        int estate_idx = -1; // for baron
        
        log_append ("\tCurrent hand: ");
        for (int i = 0; i < g->handCount[cplayer]; i++)
        {
            int card = g->hand[cplayer][i];
            
            char name[MAX_STRING_LENGTH];
            cardNumToName (card, name);
            log_append ("%s ", name);
            
            if ((card != remodel || remodel_idx != -1) && cheapest_available_card_cost <= getCost (card) + 2)
                remodelable_card_positions[num_remodelable_cards++] = i;
            
            if (card >= copper && card <= gold && cheapest_available_treasure_cost <= getCost (card) + 3)
                minable_card_positions[num_minable_cards++] = i;
            
#define SPECIAL_CARD(name) \
            if (card == name) \
            {\
                if (name##_idx == -1) \
                    name##_idx = i; \
            }\
            else
            
            SPECIAL_CARD (remodel)
            SPECIAL_CARD (mine)
            SPECIAL_CARD (salvager)
            SPECIAL_CARD (ambassador)
            SPECIAL_CARD (feast)
            // SPECIAL_CARD adds an "else" here
            if (card == treasure_map && treasuremap_idx1 == -1)
                treasuremap_idx1 = i;
            else if (card == treasure_map && treasuremap_idx2 == -1)
                treasuremap_idx2 = i;
            else if (card == estate && estate_idx == -1)
                estate_idx = i;
            else if (card >= MIN_KINGDOM && card < MAX_CARD && card != gardens)
                // card is a playable action card
                action_card_positions[num_action_cards++] = i;
        }
        log_append ("\n");
        
        // Add a random chance of choosing not to play an action card at all.
        // The chance of doing this will be 100% if the player's hand has no
        // action cards in it.
        action_card_positions[num_action_cards++] = -1;
        
        // If there is a remodel card and at least one card which is a valid
        // choice for being remodeled, add a random chance of picking the 
        // remodel card.
        if (remodel_idx != -1 && num_remodelable_cards != 0)
            action_card_positions[num_action_cards++] = remodel_idx;
        
        // If there is a mine card and at least one treasure card which is a
        // valid choice for being mined, add a random chance of picking the
        // mine card.
        if (mine_idx != -1 && num_minable_cards != 0)
            action_card_positions[num_action_cards++] = mine_idx;
        
        // If there is a salvager card and at least one other card in the
        // hand, add a random chance of picking the salvager card.
        if (salvager_idx != -1 && g->handCount[cplayer] != 1)
            action_card_positions[num_action_cards++] = salvager_idx;
        
        // If there is a feast card and at least one buyable card worth 5 
        // coins or less, add a random chance of picking the feast card.
        if (feast_idx != -1 && cheapest_available_card_cost <= 5)
            action_card_positions[num_action_cards++] = feast_idx;
        
        // If there are at least two treasure maps in the player's hand, add a
        // random chance of picking one of them.
        if (treasuremap_idx2 != -1)
            action_card_positions[num_action_cards++] = treasuremap_idx2;
        
        // Pick what action card to play.
        int action_card_to_play = action_card_positions[rand () % num_action_cards];
        
        if (action_card_to_play == -1)
            return; // End the action phase.
        
        // A card is moving from the player's hand into the played cards pile
        expected_numplayed++;
        expected_deckcount--;
        
        char name[MAX_STRING_LENGTH];
        int card = g->hand[cplayer][action_card_to_play];
        cardNumToName (card, name);
        log_append ("\tPlayer %d plays card %s\n", cplayer, name);
        
        // Generate choice variables if appropriate.
        int choice1 = -1, choice2 = -1, choice3 = -1;
        
        if (card == remodel)
        {
            choice1 = remodelable_card_positions[rand () % num_remodelable_cards];
            int card = g->hand[cplayer][choice1];
            choice2 = pick_buyable_card (0, MAX_CARD, getCost (card) + 2, g);
            cardNumToName (card, name);
            log_append ("\t\tTrash %s", name);
            cardNumToName (choice2, name);
            log_append (" in favor of %s\n", name);
        }
        else if (card == mine)
        {
            choice1 = minable_card_positions[rand () % num_minable_cards];
            int card = g->hand[cplayer][choice1];
            choice2 = pick_buyable_card (copper, gold + 1, getCost (card) + 3, g);
            cardNumToName (card, name);
            log_append ("\t\tTrash %s", name);
            cardNumToName (choice2, name);
            log_append (" in favor of %s\n", name);
        }
        else if (card == salvager)
        {
            choice1 = (salvager_idx + 1 + rand () % (g->handCount[cplayer] - 1)) % g->handCount[cplayer];
            int card = g->hand[cplayer][choice1];
            cardNumToName (card, name);
            log_append ("\t\tTrash %s for %d coins\n", name, getCost (card));
            expected_deckcount--; // A card is being trashed
        }
        else if (card == ambassador)
        {
            // Currently we don't bother checking how many of the cards we're
            // trashing are in the current hand, so we can't test trashing two
            // of them.
            choice1 = (ambassador_idx + 1 + rand () % (g->handCount[cplayer] - 1)) % g->handCount[cplayer];
            choice2 = rand () % 2;
            int card = g->hand[cplayer][choice1];
            cardNumToName (card, name);
            log_append ("\t\tMove %d %s card(s) back to supply\n", choice2, name);
            expected_deckcount -= choice2; // Cards are being moved into the supply
        }
        else if (card == steward)
        {
            if (g->handCount[cplayer] > 2)
                choice1 = 1 + rand () % 3;
            else
                choice1 = 1 + rand () % 2;
            
            if (choice1 == 1)
            {
                log_append ("\t\tDraw 2\n");
            }
            else if (choice1 == 2)
            {
                log_append ("\t\t+2 coins\n");
            }
            else if (choice1 == 3)
            {
                choice2 = rand () % (g->handCount[cplayer] - 1);
                choice3 = rand () % (g->handCount[cplayer] - 2); 
                if (choice3 >= choice2)
                    choice3++;
                choice2 = (action_card_to_play + 1 + choice2) % g->handCount[cplayer];
                choice3 = (action_card_to_play + 1 + choice3) % g->handCount[cplayer];
                int card = g->hand[cplayer][choice2];
                cardNumToName (card, name);
                log_append ("\t\tTrash one %s", name);
                card = g->hand[cplayer][choice3];
                cardNumToName (card, name);
                log_append (" and one %s\n", name);
                expected_deckcount -= 2; // Two cards are being trashed
            }
        }
        else if (card == feast)
        {
            choice1 = pick_buyable_card (0, MAX_CARD, 5, g);
            cardNumToName (choice1, name);
            log_append ("\t\tGain %s\n", name);
            expected_numplayed--; // The feast is being trashed
            expected_deckcount++; // A card is being gained
        }
        else if (card == embargo)
        {
            choice1 = rand () % (MIN_KINGDOM + NUM_SUPPLY);
            if (choice1 >= MIN_KINGDOM)
                choice1 = kingdom[choice1 - MIN_KINGDOM];
            cardNumToName (choice1, name);
            log_append ("\t\tEmbargo on %s\n", name);
            expected_numplayed--; // The embargo is being trashed
        }
        else if (card == baron)
        {
            if (estate_idx != -1)
                choice1 = rand () % 2;
            else
                choice1 = 0;
            
            if (choice1 == 1)
            {
                log_append ("\t\tDiscard an estate for 4 coins.\n");
            }
            else
            {
                if (g->supplyCount[estate] > 0)
                {
                    log_append ("\t\tGain an estate.\n");
                    expected_deckcount++; // A card is being gained
                }
                else
                {
                    log_append ("\t\t(No estates left.)\n");
                }
            }
        }
        else if (card == minion)
        {
            // This is a silly way to implement the choice, but dominion-base
            // uses both choice1 and choice2 insted of just choice1.
            choice1 = rand () % 2;
            choice2 = !choice1;
            if (choice1)
            {
                log_append ("\t\t+2 Coins\n");
            }
            else
            {
                log_append ("\t\tdiscard hand, redraw 4, other players with 5+ cards discard hand and draw 4\n");
                // Hand is being discarded
                expected_numplayed += g->handCount[cplayer] - 1;
                expected_deckcount -= g->handCount[cplayer] - 1;
            }
        }
        
        int result = playCard (action_card_to_play, choice1, choice2, choice3, g);
        
        if (result < 0)
        {
            always_printf ("Turn FAILED:\n");
            log_print ();
            log_flush ();
            return;
        }
        
        // Check if any cards have gone missing or mysteriously appeared
        int actual_numplayed = g->playedCardCount; 
        int actual_deckcount =  g->handCount[cplayer] +
                                g->deckCount[cplayer] +
                                g->discardCount[cplayer];
        
        if (actual_numplayed != expected_numplayed)
        {
            always_printf ("Turn FAILED, played card mismatch (got %d expected %d!)\n", actual_numplayed, expected_numplayed);
            log_print ();
            log_flush ();
            return;
        }
        
        if (actual_deckcount != expected_deckcount)
        {
            always_printf ("Turn FAILED, deck size mismatch (got %d expected %d!)\n", actual_deckcount, expected_deckcount);
            log_print ();
            log_flush ();
            return;
        }
    }
}

extern int getCost (int cardNumber);
void run_turn_buy_phase (struct gameState *g)
{
    int cplayer = g->whoseTurn;
    
    while (true) // Loop because we may end up buying multiple cards
    {
        if (g->numBuys == 0)
            return;
        
        // Pick what card to buy.
        int card_to_buy = pick_buyable_card (-1, MAX_CARD, g->coins, g);
        
        if (card_to_buy == -1)
            return; // End the action phase.
        
        char name[MAX_STRING_LENGTH];
        cardNumToName (card_to_buy, name);
        log_append ("\tPlayer %d buys card %s\n", cplayer, name);
        
        assert (buyCard (card_to_buy, g) != FAILURE);
    }
}

void run_turn (struct gameState *g)
{
    log_append ("Player %d's turn\n", g->whoseTurn);
    run_turn_action_phase (g);
    run_turn_buy_phase (g);
    endTurn (g);
    
    for (int i = 0; i < g->numPlayers; i++)
    {
        int numInvalid = fullDeckCount (i, -1, g);
        if (numInvalid != 0)
        {
            always_printf ("Turn FAILED, found %d -1 cards in player %d's possesion\n", numInvalid, i);
            log_print ();
            log_flush ();
            // May as well exit now, otherwise we'll get spammed with this
            // warning each and every turn.
            exit (EXIT_SUCCESS);
        }
    }
    
    if (verbose)
        log_print ();
    log_flush ();
}

// See http://www.cplusplus.com/reference/cstdlib/qsort/
static int compare (const void * a, const void * b)
{
    return ( *(const int*)a - *(const int*)b );
}

int main (int argc, char *argv[])
{
    struct gameState G;
    int kingdomSorted[NUM_SUPPLY];
    int seed;
    
    // Generate a random seed that incorporates entropy from both the integer
    // number of seconds since the epoch and and the integer number of
    // microseconds since the last whole second. Before, I was just using the
    // system time in seconds, but that meant I got the exact same test run if
    // I ran it twice in the same second, which I am doing in the Makefile.
    struct timeval tv;
    gettimeofday (&tv, NULL);
    seed = time (NULL) ^ tv.tv_usec;
    
    gameresults_out_file = fopen ("gameResults.out", "w");
    
    char c;
    while ((c = getopt (argc, argv, "s:v")) != -1)
    {
        switch (c)
        {
            case 'v':
                verbose = true;
                break;
            case 's':
                seed = atoi (optarg);
                break;
        }
    }
    
    always_printf ("USING RANDOM SEED %d\n", seed);
    
    srand (seed); // Seed the randomizer
    
    verbose_printf ("Selected kingdom cards: ");
    for (int i = 0; i < NUM_SUPPLY; i++)
    {
        // If i kingdom card types have already been selected, we have 
        // NUM_KINGDOM - i remaining kingdom card types to choose from without
        // chosing the same card type twice.
        int which_kingdom = rand () % (NUM_KINGDOM - i) + MIN_KINGDOM;
        for (int j = 0; j < i; j++)
        {
            if (which_kingdom >= kingdomSorted[j])
                which_kingdom++;
        }
        kingdomSorted[i] = kingdom[i] = which_kingdom;
        qsort (kingdomSorted, i+1, sizeof(int), compare);
        char name[MAX_STRING_LENGTH];
        cardNumToName (which_kingdom, name);
        verbose_printf ("%s ", name);
    }
    verbose_printf ("\n");
    
    int nplayers = 2 + rand () % 3;
    
    verbose_printf ("%d-player game.\n", nplayers);
    
    assert (initializeGame (nplayers, kingdom, rand (), &G) != FAILURE);
    
    while (!isGameOver (&G))
        run_turn (&G);
    
    always_printf ("Finished game.\n")
    for (int i = 0; i < nplayers; i++)
        always_printf ("Player %d: %d\n", i, scoreFor (i, &G));
    
    return EXIT_SUCCESS;
}
